import 'dart:ffi';
import 'dart:io';
import 'dart:isolate';

import 'package:ffi/ffi.dart';
import 'package:flutter/foundation.dart';
import 'package:json_serializer/json_serializer.dart';
import 'package:win32/win32.dart';

LPSTR wsalloc2(int wChars) => calloc<UCHAR>(wChars).cast();

abstract class AbstractClient {

  int cmdPipeHandle = INVALID_HANDLE_VALUE;
  int eventPipeHandle = INVALID_HANDLE_VALUE;

  bool connected = false;
  bool reconnecting = false;
  bool cleanShutdown = false;

  String get cmdPipeName;

  String get eventPipeName;

  Future<void> connectAsync() async {
    final pipeHandles = await compute(_connectPipes, (cmdPipeName: cmdPipeName, eventPipeName: eventPipeName));
    cmdPipeHandle = pipeHandles.cmdPipeHandle;
    eventPipeHandle = pipeHandles.eventPipeHandle;

    clientConnected();
  }

  Future<void> reconnectAsync() async {
    if (reconnecting) {
      return;
    }

    reconnecting = true;

    final pipeHandles = await compute(_reconnectPipes, (cmdPipeName: cmdPipeName, eventPipeName: eventPipeName));
    cmdPipeHandle = pipeHandles.cmdPipeHandle;
    eventPipeHandle = pipeHandles.eventPipeHandle;

    clientConnected();
  }

  ({int cmdPipeHandle, int eventPipeHandle}) _reconnectPipes(({String cmdPipeName, String eventPipeName}) args) {
    final cmdPipeName = args.cmdPipeName;
    final eventPipeName = args.eventPipeName;
    
    while (true) {
      try {
        sleep(const Duration(milliseconds: 2500));

        final pipeHandles = _connectPipes((cmdPipeName: cmdPipeName, eventPipeName: eventPipeName));

        return (cmdPipeHandle: pipeHandles.cmdPipeHandle, eventPipeHandle: pipeHandles.eventPipeHandle);
      } on Exception {
        print('Reconnect failed. Trying again...');
      }
    }
  }

  ({int cmdPipeHandle, int eventPipeHandle}) _connectPipes(({String cmdPipeName, String eventPipeName}) args) {
    final lpCmdPipeName = args.cmdPipeName.toNativeUtf16();
    final lpEventPipeName = args.eventPipeName.toNativeUtf16();
    try {
      var cmdPipeHandle = CreateFile(
        lpCmdPipeName,
        GENERIC_ACCESS_RIGHTS.GENERIC_READ | GENERIC_ACCESS_RIGHTS.GENERIC_WRITE,
        FILE_SHARE_MODE.FILE_SHARE_READ | FILE_SHARE_MODE.FILE_SHARE_WRITE,
        nullptr,
        FILE_CREATION_DISPOSITION.OPEN_EXISTING,
        FILE_FLAGS_AND_ATTRIBUTES.FILE_ATTRIBUTE_NORMAL,
        NULL);
      
      if (cmdPipeHandle == INVALID_HANDLE_VALUE) {
        throw Exception('Failed to connect to command pipe.');
      }

      var eventPipeHandle = CreateFile(
        lpEventPipeName,
        GENERIC_ACCESS_RIGHTS.GENERIC_READ,
        FILE_SHARE_MODE.FILE_SHARE_READ,
        nullptr,
        FILE_CREATION_DISPOSITION.OPEN_EXISTING,
        FILE_FLAGS_AND_ATTRIBUTES.FILE_ATTRIBUTE_NORMAL,
        NULL);
      
      if (eventPipeHandle == INVALID_HANDLE_VALUE) {
        throw Exception('Failed to connect to event pipe.');
      }

      return (cmdPipeHandle: cmdPipeHandle, eventPipeHandle: eventPipeHandle);
    } finally {
      free(lpCmdPipeName);
      free(lpEventPipeName);
    }
  }

  void processLine(String line);

  void clientConnected() async {
    connected = true;
    reconnecting = false;
    cleanShutdown = false;

    final receivePort = ReceivePort();
    receivePort.listen((line) {
      processLine(line);
    });
    await Isolate.spawn(readEvent, (sendPort: receivePort.sendPort, eventPipeHandle: eventPipeHandle));
  }

  void clientDisconnected() async {
    reconnectAsync();
    connected = false;
  }

  void readEvent(({ SendPort sendPort, int eventPipeHandle }) args) {
    while (true) {
      // TODO 退出循环
      // if (false) {
      //   break;
      // }
      try {
        final respAsString = readMessage((args.eventPipeHandle,));
        args.sendPort.send(respAsString);
      } catch(e) {
        // 管道关闭，退出Isolate
        break;
      }
    }

    // TODO Disconnected
  }

  Future<void> sendAsync<T extends Serializable>(T data) async {
    var sendAsString = '${serialize(data)}\n';
    await writeMessageAsync(cmdPipeHandle, sendAsString);
  }

  Future<T> readAsync<T>() async {
    var respAsString = await readMessageAsync(cmdPipeHandle);
    return deserialize<T>(respAsString);
  }

  Future<String> readMessageAsync(int pipeHandle) async {
    return await compute(readMessage, (pipeHandle,));
  }

  Future<int> writeMessageAsync(int pipeHandle, String pipeMessage) async {
    return await compute(writeMessage, (pipeHandle, pipeMessage));
  }

  String readMessage((int,) args) {
    final pipeHandle = args.$1;
    final lpBuffer = wsalloc2(16384);
    final lpNumBytesRead = calloc<DWORD>();
    try {
      stdout.writeln('Reading data from pipe...$pipeHandle');
      final result =
          ReadFile(pipeHandle, lpBuffer.cast(), 16384, lpNumBytesRead, nullptr);
      if (result == NULL) {
        throw Exception("Failed to read data from the pipe.");
      } else {
        final numBytesRead = lpNumBytesRead.value;
        stdout
          ..writeln('Number of bytes read: $numBytesRead')
          ..writeln('Message: ${lpBuffer.toDartString()}');

        return lpBuffer.toDartString();
      }
      
    } finally {
      free(lpBuffer);
      free(lpNumBytesRead);
    }
  }

  int writeMessage((int, String) args) {
    final pipeHandle = args.$1;
    final pipeMessage = args.$2;
    final lpPipeMessage = pipeMessage.toNativeUtf8();
    final lpNumBytesWritten = calloc<DWORD>();

    try {
      final result = WriteFile(pipeHandle, lpPipeMessage.cast(), pipeMessage.length,
          lpNumBytesWritten, nullptr);

      if (result == NULL) {
        throw Exception('Failed to send data.');
      } else {
        final numBytesWritten = lpNumBytesWritten.value;
        stdout.writeln('Number of bytes sent: $numBytesWritten');

        return numBytesWritten;
      }
    } finally {
      free(lpPipeMessage);
      free(lpNumBytesWritten);
    }
  }
}